using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Collections;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Security.Permissions;
using C4F.DevKit.WebServices.Properties;
using C4F.DevKit.WebServices.Datasets;
using System.Globalization;

namespace C4F.DevKit.WebServices
{
    /// <summary>
    /// Exposes search functionality on Amazon.com
    /// </summary>
    [ToolboxItem(true)]
    public class Amazon : C4FWebServicesBase
    {
        #region Private members
        /// <summary>
        /// Base Url
        /// </summary>
        private static string baseUrl = Resources.AmazonBaseUrl;
            
        /// <summary>
        /// Application key
        /// </summary>
        private string applicationKey = string.Empty;

        #endregion

        #region Accessors

        /// <summary>
        /// Access Key required to use the Api
        /// </summary>
        [Category("Amazon")]
        [Description("Access Key required to use the Api")]
        public string ApplicationKey
        {
            get
            {
                return this.applicationKey == null ?
                string.Empty : this.applicationKey;
            }
            set
            { this.applicationKey = value == null ? string.Empty : value.Trim(); }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new object of Amazon class
        /// </summary>
        public Amazon() { }
        /// <summary>
        /// Initializes a new object of Amazon class with an application key
        /// </summary>
        /// <param name="appKey">Application key</param>
        public Amazon(string appKey)
        {
            this.ApplicationKey = appKey;
        }
        #endregion

        #region Public functions
        /// <summary>
        /// Searches for items based on keyword(s)
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="pageNo">Search Results page number (null for default value)</param>
        /// <param name="condition">Condition of the item</param>
        /// <param name="minimumPrice">Minimum price (null for default value)</param>
        /// <param name="maximumPrice">Maximum price (null for default value)</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <returns>List of Items that match the search criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set (OR) the keywords are not specified</exception>
        /// <exception cref="AmazonException">Amazon Web service has raised an error</exception>
        public List<AmazonItem> SearchItems(
            SearchCategory searchCategory, int? pageNo, ItemCondition condition,
            int? minimumPrice, int? maximumPrice, params string[] keywords)
        {
            if (String.IsNullOrEmpty(this.ApplicationKey))
                throw new InvalidOperationException(Resources.ErrorApplicationKeyNotSet);

            // Throw an exception if no keywords were specified
            if (keywords == null || keywords.Length == 0)
                throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, Resources.ErrorSpecifyArgument, "keyword"));


            string requestUrl = this.GetRequestUrl(searchCategory,
                pageNo, condition, minimumPrice, maximumPrice,
                GetKeywordString(keywords), null, null, null, null, null);

            // Get Amazon ItemSearch responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return Amazon.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Searches for items based on keyword(s)
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <returns>List of Items that match the search criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set (OR) the keywords are not specified</exception>
        /// <exception cref="AmazonException">Amazon Web service has raised an error</exception>
        public List<AmazonItem> SearchItems(
            SearchCategory searchCategory, params string[] keywords)
        {
            if (String.IsNullOrEmpty(this.ApplicationKey))
                throw new InvalidOperationException(Resources.ErrorApplicationKeyNotSet);

            // Throw an exception if no keywords were specified
            if (keywords == null || keywords.Length == 0)
                throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, Resources.ErrorSpecifyArgument, "keyword"));

            string requestUrl = this.GetRequestUrl(searchCategory, null, ItemCondition.None,
                null, null, GetKeywordString(keywords), null, null, null, null, null);

            // Get Amazon ItemSearch responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return Amazon.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Searches for items based on keyword(s)
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="condition">Condition of the item</param>
        /// <param name="minimumPrice">Minimum price (null for default value)</param>
        /// <param name="maximumPrice">Maximum price (null for default value)</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <returns>List of Items that match the search criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set (OR) the keywords are not specified</exception>
        /// <exception cref="AmazonException">Amazon Web service has raised an error</exception>
        public List<AmazonItem> SearchItems(
            SearchCategory searchCategory, ItemCondition condition,
            int? minimumPrice, int? maximumPrice, params string[] keywords)
        {
            if (String.IsNullOrEmpty(this.ApplicationKey))
                throw new InvalidOperationException(Resources.ErrorApplicationKeyNotSet);

            // Throw an exception if no keywords were specified
            if (keywords == null || keywords.Length == 0)
                throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, Resources.ErrorSpecifyArgument, "keyword"));

            string requestUrl = this.GetRequestUrl(searchCategory,
                null, condition, minimumPrice, maximumPrice,
                GetKeywordString(keywords), null, null, null, null, null);

            // Get Amazon ItemSearch responses
            string responseData = base.GetQueryResponse(requestUrl);

            // Parse the data and return results
            return Amazon.ParseResponseXml(responseData);
        }

        /// <summary>
        /// Searches for items based on the specified search criteria and returns the results in a dataset.
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="pageNo">Search Results page number (null for default value)</param>
        /// <param name="condition">Condition of the item</param>
        /// <param name="minimumPrice">Minimum price (null for default value)</param>
        /// <param name="maximumPrice">Maximum price (null for default value)</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <returns>Dataset containing list of Items that match the search criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set (OR) the keywords are not specified</exception>
        /// <exception cref="AmazonException">Amazon Web service has raised an error</exception>
        public DS_Amazon SearchItems_DS(
            SearchCategory searchCategory, int? pageNo, ItemCondition condition,
            int? minimumPrice, int? maximumPrice, params string[] keywords)
        {
            List<AmazonItem> amazonItems = SearchItems(searchCategory, pageNo, condition, minimumPrice, maximumPrice,
                keywords);
            return PopulateDataset(amazonItems);
        }

        /// <summary>
        /// Searches for items based on the specified search criteria and returns the results in a dataset.
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <returns>Dataset containing list of Items that match the search criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set (OR) the keywords are not specified</exception>
        /// <exception cref="AmazonException">Amazon Web service has raised an error</exception>
        public DS_Amazon SearchItems_DS(
            SearchCategory searchCategory, params string[] keywords)
        {
            List<AmazonItem> amazonItems = SearchItems(searchCategory, keywords);
            return PopulateDataset(amazonItems);
        }

        /// <summary>
        /// Searches for items based on the specified search criteria and returns the results in a dataset.
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="condition">Condition of the item</param>
        /// <param name="minimumPrice">Minimum price (null for default value)</param>
        /// <param name="maximumPrice">Maximum price (null for default value)</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <returns>Dataset containing list of Items that match the search criteria</returns>
        /// <exception cref="InvalidOperationException">The Application Key is not set (OR) the keywords are not specified</exception>
        /// <exception cref="AmazonException">Amazon Web service has raised an error</exception>
        public DS_Amazon SearchItems_DS(
            SearchCategory searchCategory, ItemCondition condition,
            int? minimumPrice, int? maximumPrice, params string[] keywords)
        {
            List<AmazonItem> amazonItems = SearchItems(searchCategory, condition, minimumPrice, maximumPrice,
                keywords);
            return PopulateDataset(amazonItems);
        }
        #endregion

        #region Private functions

        /// <summary>
        /// Converts the keywords array into a space separated string
        /// </summary>
        /// <param name="keywords">Array of keywords</param>
        /// <returns>String of space separated keywords</returns>
        private static string GetKeywordString(params string[] keywords)
        {
            StringBuilder keywordString = new StringBuilder();
            int index;
            for (index = 0; index < keywords.Length - 1; index++)
            {
                if (keywords[index] != null &&
                    keywords[index].Trim().Length != 0)
                {
                    keywordString.Append(keywords[index].Trim());
                    keywordString.Append("%20");
                }
            }

            // Do not put encoded space at the end
            if (keywords[index] != null && keywords[index].Trim().Length != 0)
                keywordString.Append(keywords[index].Trim());

            return keywordString.ToString();
        }

        /// <summary>
        /// Creates the request url
        /// </summary>
        /// <param name="searchCategory">Search Category</param>
        /// <param name="pageNo">Search Results page number (null for default value)</param>
        /// <param name="condition">Condition of the item</param>
        /// <param name="minimumPrice">Minimum price (null for default value)</param>
        /// <param name="maximumPrice">Maximum price (null for default value)</param>
        /// <param name="keywords">One or more keywords to search for</param>
        /// <param name="title">Title</param>
        /// <param name="artist">Artist</param>
        /// <param name="musicLabel">Music Label</param>
        /// <param name="author">Author</param>
        /// <param name="publisher">Publisher</param>
        /// <returns>Request Url</returns>
        private string GetRequestUrl(SearchCategory searchCategory, int? pageNo,
            ItemCondition condition, int? minimumPrice, int? maximumPrice,
            string keywords, string title, string artist, string musicLabel,
            string author, string publisher)
        {
            StringBuilder queryBuilder = new StringBuilder(Amazon.baseUrl);

            // Add application key
            queryBuilder.Append("&AWSAccessKeyId=" + this.ApplicationKey);

            // Add search index
            queryBuilder.Append("&SearchIndex=" + searchCategory.ToString());

            // Add page no. if specified
            if (pageNo.HasValue)
                queryBuilder.Append("&ItemPage=" + pageNo.Value);

            // Add condition if specified
            if (condition != ItemCondition.None)
                queryBuilder.Append("&Condition=" + condition.ToString());

            // Add minimum price if specified
            if (minimumPrice.HasValue)
                queryBuilder.Append("&MinimumPrice=" + minimumPrice.Value);

            // Add maximum price if specified
            if (maximumPrice.HasValue)
                queryBuilder.Append("&MaximumPrice=" + maximumPrice.Value);

            // Add keywords if specified
            if (keywords.Trim().Length != 0)
                queryBuilder.Append("&Keywords=" + keywords.Trim());

            // Add title if specified
            if (!string.IsNullOrEmpty(title))
                queryBuilder.Append("&Title=" + title.Trim());

            // Add artist if specified
            if (!string.IsNullOrEmpty(artist))
                queryBuilder.Append("&Artist=" + artist.Trim());

            // Add music label if specified
            if (!string.IsNullOrEmpty(musicLabel))
                queryBuilder.Append("&MusicLabel=" + musicLabel.Trim());

            // Add author if specified
            if (!string.IsNullOrEmpty(author))
                queryBuilder.Append("&Author=" + author.Trim());

            // Add publisher if specified
            if (!string.IsNullOrEmpty(publisher))
                queryBuilder.Append("&Publisher=" + publisher.Trim());

            return queryBuilder.ToString();
        }

        /// <summary>
        /// Parses the response data and extracts the items
        /// </summary>
        /// <param name="responseData">Response data</param>
        /// <returns>List of items</returns>
        private static List<AmazonItem> ParseResponseXml(string responseData)
        {
            XmlDocument xmlDocument = new XmlDocument();
            List<AmazonItem> searchResults = null;

            XmlNamespaceManager nsManager = new XmlNamespaceManager(xmlDocument.NameTable);
            string amazonNS = "http://webservices.amazon.com/AWSECommerceService/2005-10-05";
            nsManager.AddNamespace("amazon", amazonNS);
            xmlDocument.LoadXml(responseData);

            // Retrieve errors nodes
            XmlNodeList errorNodes = xmlDocument.GetElementsByTagName("Error", amazonNS);
            if (errorNodes.Count > 0)
                throw new AmazonException(errorNodes[0].SelectSingleNode("amazon:Code", nsManager).InnerText,
                    errorNodes[0].SelectSingleNode("amazon:Message", nsManager).InnerText);

            // Retrieve result nodes
            XmlNodeList itemNodes = xmlDocument.DocumentElement.SelectNodes("amazon:Items/amazon:Item", nsManager);

            if (itemNodes.Count == 0)
                return searchResults;

            searchResults = new List<AmazonItem>(itemNodes.Count);

            for (int i = 0; i < itemNodes.Count; i++)
            {
                XmlNode itemNode = itemNodes[i];
                AmazonItem amazonItem = new AmazonItem();

                // Populate AmazonItem object
                amazonItem.Asin = itemNode.SelectSingleNode("amazon:ASIN", nsManager).InnerText;
                amazonItem.Url = itemNode.SelectSingleNode("amazon:DetailPageURL", nsManager).InnerText;
                XmlNode itemAttributesNode = itemNode.SelectSingleNode("amazon:ItemAttributes", nsManager);
                foreach (XmlNode itemAttribute in itemAttributesNode.ChildNodes)
                {
                    if (!amazonItem.ItemAttributes.ContainsKey(itemAttribute.Name))
                        amazonItem.ItemAttributes.Add(itemAttribute.Name, new ArrayList());
                    amazonItem.ItemAttributes[itemAttribute.Name].Add(itemAttribute.InnerText);
                }
                searchResults.Add(amazonItem);
            }

            return searchResults;
        }

        /// <summary>
        /// Returns a dataset populated with the specified Amazon Items.
        /// </summary>
        /// <param name="amazonItems">List of AmazonItem objects</param>
        /// <returns>Dataset containing list of Amazon Items</returns>
        private static DS_Amazon PopulateDataset(List<AmazonItem> amazonItems)
        {
            DS_Amazon dataset = new DS_Amazon();

            foreach (AmazonItem item in amazonItems)
            {
                DS_Amazon.AmazonItemRow amazonItemRow = dataset.AmazonItem.AddAmazonItemRow(item.Asin, item.Url);
                foreach (KeyValuePair<string, ArrayList> itemAttribute in item.ItemAttributes)
                {
                    DS_Amazon.ItemAttributeRow itemAttributeRow = null;
                    foreach (DS_Amazon.ItemAttributeRow row in dataset.ItemAttribute.Rows)
                    {
                        if (row.Name == itemAttribute.Key)
                        {
                            itemAttributeRow = row;
                            break;
                        }
                    }

                    if (itemAttributeRow == null)
                        itemAttributeRow = dataset.ItemAttribute.AddItemAttributeRow(itemAttribute.Key);

                    dataset.AmazonItem_ItemAttribute_Relation.AddAmazonItem_ItemAttribute_RelationRow(
                        amazonItemRow, itemAttributeRow);

                    foreach (string itemAttributeValue in itemAttribute.Value)
                        dataset.ItemAttributeValue.AddItemAttributeValueRow(itemAttributeValue, itemAttributeRow.Id,
                            amazonItemRow.Asin);
                }
            }
            return dataset;

        }
        #endregion
    }

    /// <summary>
    /// Represents an item published on Amazon.com
    /// </summary>
    public class AmazonItem
    {
        #region Private Members
        /// <summary>
        /// Item Id
        /// </summary>
        private string asin;

        /// <summary>
        /// Url with the details about the item
        /// </summary>
        private string url;

        /// <summary>
        /// Attributes of the item
        /// </summary>
        private Dictionary<string, ArrayList> itemAttributes = new Dictionary<string, ArrayList>();
        #endregion

        #region Public Properties
        /// <summary>
        /// Item Id
        /// </summary>
        public string Asin
        {
            get { return this.asin; }
            set { this.asin = value; }
        }
        /// <summary>
        /// Url with the details about the item
        /// </summary>
        public string Url
        {
            get { return this.url; }
            set { this.url = value; }
        }
        /// <summary>
        /// Attributes of the item
        /// </summary>
        public Dictionary<string, ArrayList> ItemAttributes
        {
            get { return this.itemAttributes; }
        }
        #endregion
    }

    /// <summary>
    /// The condition of the items to be returned in the search
    /// </summary>
    public enum ItemCondition
    {
        /// <summary>
        /// Represents all items
        /// </summary>
        All,

        /// <summary>
        /// Represents new items
        /// </summary>
        New,

        /// <summary>
        /// Represents used items
        /// </summary>
        Used,

        /// <summary>
        /// Represents refurbished items
        /// </summary>
        Refurbished,

        /// <summary>
        /// Represents collectible items
        /// </summary>
        Collectible,

        /// <summary>
        /// Represents all items
        /// </summary>
        None
    }

    /// <summary>
    /// Search Category
    /// </summary>
    public enum SearchCategory
    {
        /// <summary>
        /// Represents search for apparel
        /// </summary>
        Apparel,
        /// <summary>
        /// Represents search for automotives
        /// </summary>
        Automotive,
        /// <summary>
        /// Represents search for baby products
        /// </summary>
        Baby,
        /// <summary>
        /// Represents search for beauty products
        /// </summary>
        Beauty,
        /// <summary>
        /// Represents search for any products
        /// </summary>
        Blended,
        /// <summary>
        /// Represents search for books
        /// </summary>
        Books,
        /// <summary>
        /// Represents search for classical products
        /// </summary>
        Classical,
        /// <summary>
        /// Represents search for DVDs
        /// </summary>
        DVD,
        /// <summary>
        /// Represents search for digital music
        /// </summary>
        DigitalMusic,
        /// <summary>
        /// Represents search for electronics
        /// </summary>
        Electronics,
        /// <summary>
        /// Represents search for gourmet food
        /// </summary>
        GourmetFood,
        /// <summary>
        /// Represents search for health and personal care products
        /// </summary>
        HealthPersonalCare,
        /// <summary>
        /// Represents search for jewelry
        /// </summary>
        Jewelry,
        /// <summary>
        /// Represents search for kitchen products
        /// </summary>
        Kitchen,
        /// <summary>
        /// Represents search for magazines
        /// </summary>
        Magazines,
        /// <summary>
        /// Represents search for market place products
        /// </summary>
        Marketplace,
        /// <summary>
        /// Represents search for merchants
        /// </summary>
        Merchants,
        /// <summary>
        /// Represents search for miscellaneous items
        /// </summary>
        Miscellaneous,
        /// <summary>
        /// Represents search for music
        /// </summary>
        Music,
        /// <summary>
        /// Represents search for music tracks
        /// </summary>
        MusicTracks,
        /// <summary>
        /// Represents search for musical instruments
        /// </summary>
        MusicalInstruments,
        /// <summary>
        /// Represents search for office products
        /// </summary>
        OfficeProducts,
        /// <summary>
        /// Represents search for outdoor living products
        /// </summary>
        OutdoorLiving,
        /// <summary>
        /// Represents search for PC hardware 
        /// </summary>
        PCHardware,
        /// <summary>
        /// Represents search for pet supplies
        /// </summary>
        PetSupplies,
        /// <summary>
        /// Represents search for photos
        /// </summary>
        Photo,
        /// <summary>
        /// Represents search for restaurants
        /// </summary>
        Restaurants,
        /// <summary>
        /// Represents search for software
        /// </summary>
        Software,
        /// <summary>
        /// Represents search for sporting goods
        /// </summary>
        SportingGoods,
        /// <summary>
        /// Represents search for tools
        /// </summary>
        Tools,
        /// <summary>
        /// Represents search for toys
        /// </summary>
        Toys,
        /// <summary>
        /// Represents search for VHS
        /// </summary>
        VHS,
        /// <summary>
        /// Represents search for video
        /// </summary>
        Video,
        /// <summary>
        /// Represents search for video games
        /// </summary>
        VideoGames,
        /// <summary>
        /// Represents search for wireless products
        /// </summary>
        Wireless,
        /// <summary>
        /// Represents search for wireless accessories
        /// </summary>
        WirelessAccessories
    }

    /// <summary>
    /// Exception Class to handle exceptions
    /// </summary>
    [global::System.Serializable]
    public class AmazonException : Exception
    {
        /// <summary>
        /// Error code from error node
        /// </summary>
        private string errorCode;
        /// <summary>
        /// Required to throw the specific exception
        /// </summary>
        public string ErrorCode
        {
            get { return errorCode; }
            set { this.errorCode = value; }
        }
        /// <summary>
        /// Initializes a new object of AmazonException class
        /// </summary>
        public AmazonException()
        { }
        /// <summary>
        /// Initializes a new object of AmazonException class
        /// </summary>
        /// <param name="errorCode">The code that represents the error</param>
        /// <param name="message">The message that describes the error</param>
        public AmazonException(string errorCode, string message)
            : base(message)
        {
            this.ErrorCode = errorCode;
        }
        /// <summary>
        /// Initializes a new object of AmazonException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        public AmazonException(string message) : base(message) { }
        /// <summary>
        /// Initializes a new object of AmazonException class
        /// </summary>
        /// <param name="errorCode">The code that represents the error</param>
        /// <param name="message">The message that describes the error</param>
        /// <param name="inner">The exception that is the cause of the current exception</param>
        public AmazonException(string errorCode, string message, Exception inner)
            : base(message, inner) 
        {
            this.ErrorCode = errorCode;
        }
        /// <summary>
        /// Initializes a new object of AmazonException class
        /// </summary>
        /// <param name="message">The message that describes the error</param>
        /// <param name="inner">The exception that is the cause of the current exception</param>
        public AmazonException(string message, Exception inner) : base(message, inner) { }
        /// <summary>
        /// Initializes a new object of AmazonException class
        /// </summary>
        /// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized
        /// object data about the exception being thrown.</param>
        /// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual
        /// information about the source or destination.</param>
        protected AmazonException(SerializationInfo info, StreamingContext context)
            : base(info, context) {
                this.ErrorCode = info.GetString("errorCode");
        }
        /// <summary>
        /// Sets the System.Runtime.Serialization.SerializationInfo
        /// with information about the exception.
        /// </summary>
        /// <param name="info">The System.Runtime.Serialization.SerializationInfo that holds the serialized
        /// object data about the exception being thrown.</param>
        /// <param name="context">The System.Runtime.Serialization.StreamingContext that contains contextual
        /// information about the source or destination.</param>
        [SecurityPermissionAttribute(SecurityAction.Demand,
            SerializationFormatter = true)]
        public override void GetObjectData(
            SerializationInfo info, StreamingContext context)
        {
            info.AddValue("errorCode", this.ErrorCode);
            base.GetObjectData(info, context);
        }

    }
}
